package org.gauth4me.core;

import java.util.Calendar;
import java.util.TimeZone;

/**
 * Implements a TOTP generator for Google Authenticator.
 * <p/>
 * TOTP stands for time-based one-time password. Details could be found in
 * <a href="http://tools.ietf.org/html/rfc6238">RFC 6238</a>.
 */
public class GoogleTotp implements Otp {
  /** Default output OTP digit count (six). */
  protected int DIGIT_COUNT = 6;

  /** Default timer interval in seconds (30 seconds). */
  protected int TIMER_INTERVAL = 30;

  /** Default timer shift in seconds (0 seconds). */
  protected int TIMER_SHIFT = 0;

  private Mac mac;
  private Timer timer = new DefaultTimer();

  /**
   * Creates an instance of TOTP generator with given secret key.
   *
   * @param secret the secret key
   */
  public GoogleTotp(String secret) {
    mac = new Hmac(TokenDecoder.decode(secret));
  }

  public String getNextCode() {
    byte[] macResult = mac.sign(timer.getTimestamp());
    long key = dynamicTruncate(macResult);
    return toPaddedString(key);
  }

  /**
   * Sets the digit count of the output OTP.
   * <p/>
   * Default is 6.
   *
   * @param number the new number of output digits (must be between 6 and 9 inclusively)
   * @throws IllegalArgumentException if the number is not between 6 and 9 inclusively
   */
  protected void setDigitCount(int number) {
    if ((number >= 6) && (number <= 9)) {
      DIGIT_COUNT = number;
    }
    else {
      throw new IllegalArgumentException("number is " + number + " but should be between 6 and 9");
    }
  }

  /**
   * Sets the timer interval used for calculations.
   * <p/>
   * Default is 30 seconds.
   *
   * @param interval the new timer interval in seconds
   * @throws IllegalArgumentException if the interval is negative or equals zero
   */
  protected void setTimerInterval(int interval) {
    if (interval > 0) {
      TIMER_INTERVAL = interval;
    }
    else {
      throw new IllegalArgumentException("interval is " + interval + " but should be >= 0");
    }
  }

  /**
   * Sets the timer Unix time shift.
   * <p/>
   * Default is 0.
   * <p/>
   * Setting this to be greater than current Unix time results in undefined behavior.
   *
   * @param shift the new timer shift in seconds
   */
  protected void setTimerShift(int shift) {
    TIMER_SHIFT = shift;
  }

  private static final int[] POW_10 =
    {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000};

  /**
   * Performs a dynamic truncation of a byte array.
   * <p/>
   * See details in <a href="http://tools.ietf.org/html/rfc4226#page-6">RFC 4226</a>.
   *
   * @param data the byte array to be truncated
   * @return resulting number truncated to {@link GoogleTotp#DIGIT_COUNT} digits
   */
  private int dynamicTruncate(byte[] data) {
    int offset = data[data.length - 1] & 0xF;
    int rawData = (((int) data[offset    ] & 0x7F) << 24)
                | (((int) data[offset + 1] & 0xFF) << 16)
                | (((int) data[offset + 2] & 0xFF) << 8)
                | (((int) data[offset + 3] & 0xFF));
    return rawData % POW_10[DIGIT_COUNT];
  }

  /**
   * Pads a number with leading zeros.
   *
   * @param number the number to pad
   * @return string of length {@link GoogleTotp#DIGIT_COUNT} containing representation
   *         of the given number along with padding zeros as needed
   */
  private String toPaddedString(long number) {
    StringBuffer buffer = new StringBuffer(DIGIT_COUNT);
    String numString = Long.toString(number);
    for (int i = 0; i < DIGIT_COUNT - numString.length(); ++i) {
      buffer.append('0');
    }
    buffer.append(numString);
    return buffer.toString();
  }

  /**
   * Injects the specified timer.
   *
   * @param timer the new timer
   */
  void setTimer(Timer timer) {
    this.timer = timer;
  }

  /**
   * Injects the specified key.
   *
   * @param key the new key
   */
  void setSecretKey(byte[] key) {
    mac.setKey(key);
  }

  /**
   * Represents a time counter generator.
   * <p/>
   * (Present for testing purposes alongside with {@link GoogleTotp#setTimer(Timer)}.)
   */
  interface Timer {
    /**
     * Calculates the appropriate timestamp.
     *
     * Also breaks the resulting 64-bit number into an 8-byte array.
     *
     * @return 8-byte array, containing timestamp
     */
    byte[] getTimestamp();
  }

  /**
   * Implements a normal GMT-based time counter.
   */
  class DefaultTimer implements Timer {
    public byte[] getTimestamp() {
      final long trueUnixTime = getUnixTime() - TIMER_SHIFT;
      long timeCounter = trueUnixTime / TIMER_INTERVAL;
      byte[] result = new byte[8];
      for (int i = 7; i >= 0 && timeCounter != 0; --i) {
        result[i] = (byte) (timeCounter & 0xFF);
        timeCounter >>= 8;
      }
      return result;
    }

    private long getUnixTime() {
      return Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTime().getTime() / 1000;
    }
  }
}
